#include "GstMPlayVideoSinkBin.h"

#include <glib-object.h>


GST_DEBUG_CATEGORY_STATIC(gst_mplay_video_sink_bin_debug);
#define GST_CAT_DEFAULT gst_mplay_video_sink_bin_debug

/************** PLUGIN CLASS DECLARE **************/

typedef struct _GstMPlayVideoSinkElement GstMPlayVideoSinkElement;

typedef struct _GstMPlayVideoSinkBin {
    GstBin bin;             // 继承Bin容器
    GstElement *glUpload;   // 上传视频帧为OpenGL纹理
    GstElement *qmlglSink;  // 渲染OpenGL纹理到QML
} GstMPlayVideoSinkBin;

typedef struct _GstMPlayVideoSinkBinClass {
    GstBinClass parent_class;
} GstMPlayVideoSinkBinClass;

static GstBinClass *parent_class;

/// MACRO DEFINE

#define GST_TYPE_VIDEO_SINK_BIN \
    (_mvsb_get_type())

#define GST_MPLAY_VIDEO_SINK_BIN_CAST(obj) \
    ((GstMPlayVideoSinkBin *)(obj))

#define GST_MPLAY_VIDEO_SINK_BIN(obj) \
    (G_TYPE_CHECK_INSTANCE_CAST(obj, GST_TYPE_VIDEO_SINK_BIN, GstMPlayVideoSinkBin))

#define GST_MPLAY_VIDEO_SINK_BIN_CLASS(klass) \
    (G_TYPE_CHECK_INSTANCE_CAST(klass, GST_TYPE_VIDEO_SINK_BIN, GstMPlayVideoSinkBinClass))

#define GST_IS_VIDEO_SINK_BIN(obj) \
    (G_TYPE_CHECK_INSTANCE_TYPE(obj, GST_TYPE_VIDEO_SINK_BIN))

#define GST_IS_VIDEO_SINK_BIN_CLASS(klass) \
    (G_TYPE_CHECK_CLASS_TYPE(klass, GST_TYPE_VIDEO_SINK_BIN))

/// PROPERTY INFO DEFINE

enum {
    PROP_0,
    PROP_ENABLE_LAST_SAMPLE,    // 启用缓存最后一帧
    PROP_LAST_SAMPLE,           // 获取最后一帧采样
    PROP_WIDGET,                // 指向 QQuickItem*，用于渲染目标
    PROP_FORCE_ASPECT_RATIO,    // 是否保持原始宽高比
    PROP_PIXEL_ASPECT_RATIO,    // 像素宽高比
    PROP_SYNC                   // 是否与系统时钟同步渲染帧
};

#define PROP_ENABLE_LAST_SAMPLE_NAME    "enable-last-sample"
#define PROP_LAST_SAMPLE_NAME           "last-sample"
#define PROP_WIDGET_NAME                "widget"
#define PROP_FORCE_ASPECT_RATIO_NAME    "force-aspect-ratio"
#define PROP_PIXEL_ASPECT_RATIO_NAME    "pixel-aspect-ratio"
#define PROP_SYNC_NAME                  "sync"

#define DEFAULT_ENABLE_LAST_SAMPLE TRUE
#define DEFAULT_FORCE_ASPECT_RATIO TRUE
#define DEFAULT_PAR_N 0
#define DEFAULT_PAR_D 1
#define DEFAULT_SYNC TRUE

/// CLASS METHOD DECLARE

static GType _mvsb_get_type(void);
static void _mvsb_init(GstMPlayVideoSinkBin *mvsb);
static void _mvsb_class_init(GstMPlayVideoSinkBinClass *klass);
static void _mvsb_dispose(GObject *object);
static void _mvsb_get_property(GObject *object,
                               guint propId,
                               GValue *value,
                               GParamSpec *pspec);
static void _mvsb_set_property(GObject *object,
                               guint propId,
                               const GValue *value,
                               GParamSpec *pspec);
static gboolean _mvsb_sink_pad_query(GstPad* pad,
                                    GstObject* parent,
                                    GstQuery* query);

/// CLASS METHOD DEFINE

static GType _mvsb_get_type(void)
{
    static GType _mvsb_type = 0;
    if (!_mvsb_type) {
        static const GTypeInfo _mvsb_info = {
            sizeof(GstMPlayVideoSinkBinClass),
            NULL,
            NULL,
            (GClassInitFunc)_mvsb_class_init,
            NULL,
            NULL,
            sizeof(GstMPlayVideoSinkBin),
            0,
            (GInstanceInitFunc)_mvsb_init,
            NULL
        };
        _mvsb_type = g_type_register_static(GST_TYPE_BIN,
                                            "MPlayVideoSinkBin",
                                            &_mvsb_info,
                                            (GTypeFlags)0);
    }
    return _mvsb_type;
}

static void _mvsb_class_init(GstMPlayVideoSinkBinClass *klass)
{
    GObjectClass *gobject_klass;
    GstElementClass *gstelement_klass;

    gobject_klass = (GObjectClass *)klass;
    gstelement_klass = (GstElementClass *)klass;

    parent_class = (GstBinClass *)g_type_class_peek_parent(klass);

    /// 注册插件类相关类方法
    // 析构函数
    gobject_klass->dispose = _mvsb_dispose;
    // 代理属性函数
    gobject_klass->get_property = _mvsb_get_property;
    gobject_klass->set_property = _mvsb_set_property;

    // 注册属性
    g_object_class_install_property(gobject_klass, PROP_ENABLE_LAST_SAMPLE,
                                    g_param_spec_boolean(PROP_ENABLE_LAST_SAMPLE_NAME, "Enable Last Buffer",
                                                         "Enable the last-sample property", DEFAULT_ENABLE_LAST_SAMPLE,
                                                         (GParamFlags)(G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)));

    g_object_class_install_property(gobject_klass, PROP_LAST_SAMPLE,
                                    g_param_spec_boxed(PROP_LAST_SAMPLE_NAME, "Last Sample",
                                                       "The last sample received in the sink", GST_TYPE_SAMPLE,
                                                       (GParamFlags)(G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)));

    g_object_class_install_property(gobject_klass, PROP_WIDGET,
                                    g_param_spec_pointer(PROP_WIDGET_NAME, "QQuickItem",
                                                         "The QQuickItem to place in the object hierarchy",
                                                         (GParamFlags)(G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)));

    g_object_class_install_property(gobject_klass, PROP_FORCE_ASPECT_RATIO,
                                    g_param_spec_boolean(PROP_FORCE_ASPECT_RATIO_NAME, "Force aspect ratio",
                                                         "When enabled, scaling will respect original aspect ratio",
                                                         DEFAULT_FORCE_ASPECT_RATIO,
                                                         (GParamFlags)(G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)));

    g_object_class_install_property(gobject_klass, PROP_PIXEL_ASPECT_RATIO,
                                    gst_param_spec_fraction(PROP_PIXEL_ASPECT_RATIO_NAME, "Pixel Aspect Ratio",
                                                            "The pixel aspect ratio of the device", DEFAULT_PAR_N, DEFAULT_PAR_D,
                                                            G_MAXINT, 1, 1, 1,
                                                            (GParamFlags)(G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)));

    g_object_class_install_property(gobject_klass, PROP_SYNC,
                                    g_param_spec_boolean(PROP_SYNC_NAME, "Sync",
                                                         "Sync on the clock", DEFAULT_SYNC,
                                                         (GParamFlags)(G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)));

    // 设置插件元数据
    gst_element_class_set_static_metadata(gstelement_klass,
                                          "MPlay Video Sink Bin", "Sink/Video/Bin",
                                          "Video rendering for MPlay",
                                          "Mikelucis <liushijie2014@hotmail.com>");
}

static void _mvsb_init(GstMPlayVideoSinkBin *mvsb)
{
    gboolean    initialized     = FALSE;
    GstElement  *glColorConvert = NULL;
    GstPad      *pad            = NULL;

    do {
        if ((mvsb->glUpload = gst_element_factory_make("glupload", NULL)) == NULL) {
            GST_ERROR_OBJECT(mvsb, __FUNCTION__ "make 'glupload' failed");
            break;
        }
        if ((mvsb->qmlglSink = gst_element_factory_make("qmlglsink", NULL)) == NULL) {
            GST_ERROR_OBJECT(mvsb, __FUNCTION__ "make 'qmlglsink' failed");
            break;
        }
        if ((glColorConvert = gst_element_factory_make("glcolorconvert", NULL)) == NULL) {
            GST_ERROR_OBJECT(mvsb, __FUNCTION__ "make 'glcolorconvert' failed");
            break;
        }
        if ((pad = gst_element_get_static_pad(mvsb->glUpload, "sink")) == NULL) {
            GST_ERROR_OBJECT(mvsb, __FUNCTION__ "get glupload sink pad failed");
            break;
        }

        gst_object_ref(mvsb->glUpload);
        gst_object_ref(mvsb->qmlglSink);

        gst_bin_add_many(GST_BIN(mvsb), mvsb->glUpload, glColorConvert, mvsb->qmlglSink, NULL);

        gboolean res = gst_element_link_many(mvsb->glUpload, glColorConvert, mvsb->qmlglSink, NULL);

        glColorConvert = NULL;

        if (!res) {
            GST_ERROR_OBJECT(mvsb, __FUNCTION__ "link any element failed");
            break;
        }

        GstPad *ghostPad = NULL;
        if ((ghostPad = gst_ghost_pad_new("sink", pad)) == NULL) {
            GST_ERROR_OBJECT(mvsb, __FUNCTION__ "create bin sink pad failed");
            break;
        }

        // GhostPad不会自动转发caps和glContext查询
        // 插入Pad特定Query拦截处理函数
        gst_pad_set_query_function(ghostPad, _mvsb_sink_pad_query);

        if (!gst_element_add_pad(GST_ELEMENT(mvsb), ghostPad)) {
            GST_ERROR_OBJECT(mvsb, __FUNCTION__ "add pad to bin failed");
            break;
        }

        initialized = TRUE;
    } while(0);

    if (pad != NULL) {
        gst_object_unref(pad);
        pad = NULL;
    }
    if (glColorConvert != NULL) {
        gst_object_unref(glColorConvert);
        glColorConvert = NULL;
    }
    if (!initialized) {
        if (mvsb->qmlglSink != NULL) {
            gst_object_unref(mvsb->qmlglSink);
            mvsb->qmlglSink = NULL;
        }
        if (mvsb->glUpload != NULL) {
            gst_object_unref(mvsb->glUpload);
            mvsb->glUpload = NULL;
        }
    }
}

static void _mvsb_dispose(GObject *object)
{
    GstMPlayVideoSinkBin *mvsb;
    mvsb = GST_MPLAY_VIDEO_SINK_BIN(object);

    if (mvsb->qmlglSink != NULL) {
        gst_object_unref(mvsb->qmlglSink);
        mvsb->qmlglSink = NULL;
    }
    if (mvsb->glUpload != NULL) {
        gst_object_unref(mvsb->glUpload);
        mvsb->glUpload = NULL;
    }

    G_OBJECT_CLASS(parent_class)->dispose(object);
}

static void _mvsb_get_property(GObject *object,
                               guint propId,
                               GValue *value,
                               GParamSpec *pspec)
{
    GstMPlayVideoSinkBin *mvsb;

    mvsb = GST_MPLAY_VIDEO_SINK_BIN(object);

    // 均向外代理qmlglsink属性
    switch (propId) {
    case PROP_ENABLE_LAST_SAMPLE:
        do {
            gboolean enable = FALSE;
            g_object_get(G_OBJECT(mvsb->qmlglSink), PROP_ENABLE_LAST_SAMPLE_NAME, &enable, NULL);
            g_value_set_boolean(value, enable);
        } while(0);
        break;
    case PROP_LAST_SAMPLE:
        do {
            GstSample *sample = NULL;
            g_object_get(G_OBJECT(mvsb->qmlglSink), PROP_LAST_SAMPLE_NAME, &sample, NULL);
            gst_value_set_sample(value, sample);
            if (sample != NULL) {
                gst_sample_unref(sample);
                sample = NULL;
            }
        } while(0);
        break;
    case PROP_WIDGET:
        do {
            gpointer widget = NULL;
            g_object_get(G_OBJECT(mvsb->qmlglSink), PROP_WIDGET_NAME, &widget, NULL);
            g_value_set_pointer(value, widget);
        } while(0);
        break;
    case PROP_FORCE_ASPECT_RATIO:
        do {
            gboolean enable = FALSE;
            g_object_get(G_OBJECT(mvsb->qmlglSink), PROP_FORCE_ASPECT_RATIO_NAME, &enable, NULL);
            g_value_set_boolean(value, enable);
        } while(0);
        break;
    case PROP_PIXEL_ASPECT_RATIO:
        do {
            gint num = 0, den = 1;
            g_object_get(G_OBJECT(mvsb->qmlglSink), PROP_PIXEL_ASPECT_RATIO_NAME, &num, &den, NULL);
            gst_value_set_fraction(value, num, den);
        } while(0);
        break;
    case PROP_SYNC:
        do {
            gboolean enable = FALSE;
            g_object_get(G_OBJECT(mvsb->qmlglSink), PROP_SYNC_NAME, &enable, NULL);
            g_value_set_boolean(value, enable);
        } while(0);
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, propId, pspec);
        break;
    }
}

static void _mvsb_set_property(GObject *object,
                               guint propId,
                               const GValue *value,
                               GParamSpec *pspec)
{
    GstMPlayVideoSinkBin *mvsb;

    mvsb = GST_MPLAY_VIDEO_SINK_BIN(object);

    // 均向外代理qmlglsink属性
    switch (propId) {
    case PROP_ENABLE_LAST_SAMPLE:
        g_object_set(G_OBJECT(mvsb->qmlglSink), PROP_ENABLE_LAST_SAMPLE_NAME, g_value_get_boolean(value), NULL);
        break;
    case PROP_WIDGET:
        g_object_set(G_OBJECT(mvsb->qmlglSink), PROP_WIDGET_NAME, g_value_get_pointer(value), NULL);
        break;
    case PROP_FORCE_ASPECT_RATIO:
        g_object_set(G_OBJECT(mvsb->qmlglSink), PROP_FORCE_ASPECT_RATIO_NAME, g_value_get_boolean(value), NULL);
        break;
    case PROP_PIXEL_ASPECT_RATIO:
        g_object_set(G_OBJECT(mvsb->qmlglSink), PROP_PIXEL_ASPECT_RATIO_NAME, gst_value_get_fraction_numerator(value), gst_value_get_fraction_denominator(value), NULL);
        break;
    case PROP_SYNC:
        g_object_set(G_OBJECT(mvsb->qmlglSink), PROP_SYNC_NAME, g_value_get_boolean(value), NULL);
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, propId, pspec);
        break;
    }
}

static gboolean
_mvsb_sink_pad_query(GstPad* pad, GstObject* parent, GstQuery* query)
{
    GstMPlayVideoSinkBin *vsb;
    GstElement* element;

    vsb = GST_MPLAY_VIDEO_SINK_BIN(parent);

    switch (GST_QUERY_TYPE(query)) {
    case GST_QUERY_CAPS:
        // 查询PadCaps情况下
        element = vsb->glUpload;
        break;
    case GST_QUERY_CONTEXT:
        // GL Context情况下
        element = vsb->qmlglSink;
        break;
    default:
        return gst_pad_query_default (pad, parent, query);
    }

    if (element == NULL) {
        GST_ERROR_OBJECT(vsb, "No element found");
        return FALSE;
    }

    // 获取Query对应组件SinkPad
    GstPad* sinkpad = gst_element_get_static_pad(element, "sink");

    if (sinkpad == NULL) {
        GST_ERROR_OBJECT(vsb, "No sink pad found");
        return FALSE;
    }

    // 发送Query到实际对应处理组件Pad
    const gboolean ret = gst_pad_query(sinkpad, query);

    gst_object_unref(sinkpad);
    sinkpad = NULL;

    return ret;
}

/************** PLUGIN EXPORT **************/

gboolean gst_mplay_video_sink_bin_plugin_init(GstPlugin *plugin)
{
    GST_DEBUG_CATEGORY_INIT(gst_mplay_video_sink_bin_debug,
                            "mplayvideosinkbin",
                            0,
                            "MPlay Video Sink Bin");
    return gst_element_register(plugin,
                                "mplayvideosinkbin",
                                GST_RANK_NONE,
                                GST_TYPE_VIDEO_SINK_BIN);
}
